/*
 * 想搞明白透视投影如何使用。采用 glm 产生透视变换矩阵时（不论这个矩阵是基于右手坐标系还是左手坐标系），最终 opengl 生成的 ndc 都是左手坐标系，可以查看打印。
 * 由于透视变换是我们自己的行为，而且不用关心如何透视变换生成 ndc ，因此我们自己定义几何图形的坐标采用左手还是右手坐标系。
 * 如下面两个例子，启用深度测试后，左边采用右手坐标系 z 轴坐标大的显示在前面。右边采用左手坐标系 z 轴坐标小的显示在前面。
 */
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <cstdio>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>

#define VERTEX_POSATTR_NUM 4
#define TRIANGLE_BYTES sizeof(GLfloat) * VERTEX_POSATTR_NUM * 3 // 一个三角形所有顶点位置属性的字节数
#define TRIANGLE_NUM 3 // 绘制的三角形的数量

const GLchar *vertexcode = 
"#version 330 core \n"
"layout(location = 0) in vec4 pos_modelspace; \n"
"out vec4 o_color; \n"
"void main() { \n"
"	gl_Position = pos_modelspace; \n"
"}";

const GLchar *fragmentcode = 
"#version 330 core \n"
"uniform vec3 bg; \n"
"out vec3 color; \n"
"in vec4 o_color; \n"
"void main() { \n"
"	color = bg; \n"
"}";

static GLuint 
load_shader(GLenum shadertype, const GLchar *shadercode) {
	GLuint shader = glCreateShader(shadertype);
	if (shader == 0) {
		printf("fail to create shader\n");
		return 0;
	}

	// 编译
	glShaderSource(shader, 1, &shadercode, NULL);
	glCompileShader(shader);

	// 检查编译结果
	GLint value = 0;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &value);
	if (value == GL_FALSE) {
		value = 0;
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &value);
		if (value > 1) {
			GLchar info[value + 1];
			glGetShaderInfoLog(shader, value, NULL, info);
			printf("fail to compile shader: %s\n", info);
		}
		glDeleteShader(shader);
		shader = 0;
	}

	return shader;
}

static GLuint 
link_program(const GLchar *vertexcode, const GLchar *fragmentcode) {
	GLuint vertexshader = 0, fragmentshader = 0, program = 0;
	vertexshader = load_shader(GL_VERTEX_SHADER, vertexcode);
	if (vertexshader == 0) {
		printf("fail to create vertex shader");
		return 0;
	}
	fragmentshader  = load_shader(GL_FRAGMENT_SHADER, fragmentcode);
	if (fragmentshader == 0) {
		printf("fail to create fragment shader");
		glDeleteShader(vertexshader);
		return 0;
	}

	program = glCreateProgram();
	if (program == 0) {
		printf("fail to create program");
		glDeleteShader(vertexshader);
		glDeleteShader(fragmentshader);
		return 0;
	}

	// 链接
	glAttachShader(program, vertexshader);
	glAttachShader(program, fragmentshader);
	glLinkProgram(program);

	// 查看链接结果
	GLint value = 0;
	glGetProgramiv(program, GL_LINK_STATUS, &value);
	if (value == GL_FALSE) {
		value = 0;
		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &value);
		if (value > 1) {
			GLchar info[value];
			glGetProgramInfoLog(program, value, NULL, info);
			printf("fail to link program: %s\n", info);
		}
		glDeleteShader(vertexshader);
		glDeleteShader(fragmentshader);
		glDeleteProgram(program);
		return 0;
	}

	printf("link program successfully, delete no use shader %d %d\n", vertexshader, fragmentshader);
	glDetachShader(program, vertexshader);
	glDetachShader(program, fragmentshader);
	glDeleteShader(vertexshader);
	glDeleteShader(fragmentshader);
	return program;
}

// 位置属性包含 4 个元素 (x, y, z, w) ，直接在这里进行矩阵计算。
void
initdraw(int len, GLfloat **multi_vertices, GLuint *boarr, int trianglebytes, const glm::mat4 &view, const glm::mat4 &perspective) {
	GLfloat *ptr, *startptr;
	int attrnum = trianglebytes / (sizeof(GLfloat) * VERTEX_POSATTR_NUM);

	printf("init draw now, [triangle num:%d] [vertex num per triangle:%d]\n", len, attrnum);
	for (int i = 0; i < len; i++) {
		printf("init draw with triangle:%d\n", i);
		GLfloat *triangle = multi_vertices[i];
		glBindBuffer(GL_ARRAY_BUFFER, boarr[i]);
		glBufferData(GL_ARRAY_BUFFER, trianglebytes, NULL, GL_STATIC_DRAW);

		startptr = ptr = (GLfloat*)glMapBufferRange(GL_ARRAY_BUFFER, 0, trianglebytes, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
		for (int j = 0; j < attrnum; j++) {
			GLfloat *vertex = triangle + j * VERTEX_POSATTR_NUM;
			ptr = startptr + j * VERTEX_POSATTR_NUM;

			glm::vec4 point(vertex[0], vertex[1], vertex[2], vertex[3]); // 位置属性包含的元素
			printf("before point:%d %10.3f,%10.3f,%10.3f,%10.3f\n", j, vertex[0], vertex[1], vertex[2], vertex[3]);
			point = perspective * view * point; // 坐标转换
			printf("after point :%d %10.3f,%10.3f,%10.3f,%10.3f\n", j, point.x, point.y, point.z, point.w);
			printf("after /w    :%d %10.3f,%10.3f,%10.3f\n", j, point.x/point.w, point.y/point.w, point.z/point.w);
			memcpy(ptr, glm::value_ptr(point), sizeof(point));
		}
		if (glUnmapBuffer(GL_ARRAY_BUFFER) == GL_FALSE)
			printf("fail to unmap buffer\n");
		glBindBuffer(GL_ARRAY_BUFFER, 0);
	}
	fflush(stdout);
}

void 
startdraw(int len, GLuint *boarr, GLuint bglocation, GLfloat **bgarr) {
	for (int i = 0; i < len; i++) {
		glUniform3fv(bglocation, 1, bgarr[i]);

		glBindBuffer(GL_ARRAY_BUFFER, boarr[i]);
		glVertexAttribPointer(0, VERTEX_POSATTR_NUM, GL_FLOAT, GL_FALSE, 0, 0);
		glDrawArrays(GL_TRIANGLES, 0, 3);	
	}
}

int 
main() {
	if (!glfwInit()) 
		return 0;

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	
	GLFWwindow *window = glfwCreateWindow(800, 600, "perspective transform", NULL, NULL);
	if (window == NULL) {
		printf("fail to create window\n");
		glfwTerminate();
		return 0;
	}
	glfwMakeContextCurrent(window);

	glewExperimental = GL_TRUE;
	if (glewInit() != GLEW_OK) {
		glfwTerminate();
		return 0;
	}

	glEnable(GL_DEPTH_TEST);

	GLuint program = link_program(vertexcode, fragmentcode);
	if (program == 0)
		return 0;
	GLuint bglocation = glGetUniformLocation(program, "bg");

	GLfloat red[] = { 1.0f,0.0f,0.0f, };
	GLfloat green[] = { 0.0f,0.5f,0.0f, };
	GLfloat blue[] = { 0.0f,0.0f,1.0f, };
	GLfloat *bgarr[] = { red, green, blue, };

#ifdef GLM_LEFT_HANDED
	printf("glm use left hand default\n");
#else
	printf("glm use right hand default\n");
#endif

	GLfloat vertices_left1[] = { -1.0f,-1.0f,0.0f,1.0f,  0.0f,-1.0f,0.0f,1.0f,  -0.5f,1.0f,0.0f,1.0f, };
	GLfloat vertices_left2[] = { -1.0f,-1.0f,3.0f,1.0f,  0.0f,-1.0f,3.0f,1.0f,  -0.5f,1.0f,3.0f,1.0f, };
	GLfloat vertices_left3[] = { -1.0f,-1.0f,6.0f,1.0f,  0.0f,-1.0f,6.0f,1.0f,  -0.5f,1.0f,6.0f,1.0f, };
	GLfloat *vertices_left[] = { vertices_left1, vertices_left2, vertices_left3 };

	glm::mat4 viewleft; 
	viewleft = glm::translate(viewleft, glm::vec3(0.0f, 0.0f, -10.0f));
	// 采用右手坐标系透视，x 指向右方，y 指向上方，z 从屏幕指向你。
	glm::mat4 perspective_righthand;
	perspective_righthand = glm::perspectiveRH(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

	GLuint vaoleft = 0;
	GLuint boarr_left[TRIANGLE_NUM];
	glGenVertexArrays(1, &vaoleft);
	glBindVertexArray(vaoleft);
		glEnableVertexAttribArray(0);
		glGenBuffers(TRIANGLE_NUM, boarr_left);
		initdraw(TRIANGLE_NUM, vertices_left, boarr_left, TRIANGLE_BYTES, viewleft, perspective_righthand);
	glBindVertexArray(0);

	printf("\n");
	GLfloat vertices_right1[] = { 0.0f,-1.0f,0.0f,1.0f,  1.0f,-1.0f,0.0f,1.0f,  0.5f,1.0f,0.0f,1.0f, };
	GLfloat vertices_right2[] = { 0.0f,-1.0f,3.0f,1.0f,  1.0f,-1.0f,3.0f,1.0f,  0.5f,1.0f,3.0f,1.0f, };
	GLfloat vertices_right3[] = { 0.0f,-1.0f,6.0f,1.0f,  1.0f,-1.0f,6.0f,1.0f,  0.5f,1.0f,6.0f,1.0f, };
	GLfloat *vertices_right[] = { vertices_right1, vertices_right2, vertices_right3 };

	glm::mat4 viewright; 
	viewright = glm::translate(viewright, glm::vec3(0.0f, 0.0f, 4.0f));
	// 采用左手坐标系透视，x 指向右方，y 指向上方，z 从你指向屏幕。
	glm::mat4 perspective_lefthand;
	perspective_lefthand = glm::perspectiveLH(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

	GLuint vaoright = 0;
	GLuint boarr_right[TRIANGLE_NUM];
	glGenVertexArrays(1, &vaoright);
	glBindVertexArray(vaoright);
		glEnableVertexAttribArray(0);
		glGenBuffers(TRIANGLE_NUM, boarr_right);
		initdraw(TRIANGLE_NUM, vertices_right, boarr_right, TRIANGLE_BYTES, viewright, perspective_lefthand);
	glBindVertexArray(0);
	
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

	GLfloat bg[3];
	while (!glfwWindowShouldClose(window)) {
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		
		glUseProgram(program);
		glBindVertexArray(vaoleft);
			startdraw(TRIANGLE_NUM, boarr_left, bglocation, bgarr);
		glBindVertexArray(0);

		glBindVertexArray(vaoright);
		 	startdraw(TRIANGLE_NUM, boarr_right, bglocation, bgarr);
		glBindVertexArray(0);
		glUseProgram(0);
		
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glDeleteProgram(program);
	glDeleteVertexArrays(1, &vaoleft);
	glDeleteBuffers(TRIANGLE_NUM, boarr_left);
	glDeleteVertexArrays(1, &vaoright);
	glDeleteBuffers(TRIANGLE_NUM, boarr_right);

	glfwTerminate();
	return 0;
}
